package org.ethereum.net.dpos;

import static java.lang.Math.max;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import org.ethereum.config.SystemProperties;
import org.ethereum.core.Block;
import org.ethereum.core.BlockHeader;
import org.ethereum.core.Blockchain;
import org.ethereum.core.BlockchainImpl;
import org.ethereum.core.ImportResult;
import org.ethereum.core.PendingState;
import org.ethereum.core.PendingStateImpl;
import org.ethereum.db.BlockStore;
import org.ethereum.db.ByteArrayWrapper;
import org.ethereum.db.IndexedBlockStore;
import org.ethereum.facade.Ethereum;
import org.ethereum.facade.EthereumImpl;
import org.ethereum.listener.CompositeEthereumListener;
import org.ethereum.net.stats.ConcludeStats;
import org.ethereum.net.stats.KPICollector;
import org.ethereum.util.ByteUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * The block generator does package all pending transactions into a new block and then broadcast to all peers.
 * 
 * @author shaolinwu 02/07/2018
 */
@Component
public class BlockGenerator {

    private static final Logger logger = LoggerFactory.getLogger("dpos");
	
    private BlockStore blockStore;
    
	private Blockchain blockchain;

    @Autowired
    private Ethereum ethereum;

    protected PendingState pendingState;
    
    private final int UNCLE_LIST_LIMIT;
    private final int UNCLE_GENERATION_LIMIT;
    
    private long startNonce = -1;
    
    private boolean isSkipEmptyBlock = false;
    
    public static final String GENERATED_BLOCK = "GeneratedBlock";
    public static final String GENERATED_INVALID_BLOCK = "GeneratedInvalidBlock";
    private final ConcludeStats blockCounter;
    private final ConcludeStats invalidBlockCounter;
    
    @Autowired
    public BlockGenerator(final SystemProperties config, final CompositeEthereumListener listener,
            final Blockchain blockchain, final BlockStore blockStore,
            final PendingState pendingState) {
        this.blockchain = blockchain;
        this.blockStore = blockStore;
        this.pendingState = pendingState;
    		UNCLE_LIST_LIMIT = config.getBlockchainConfig().getCommonConstants().getUNCLE_LIST_LIMIT();
        UNCLE_GENERATION_LIMIT = config.getBlockchainConfig().getCommonConstants().getUNCLE_GENERATION_LIMIT();
        isSkipEmptyBlock = SystemProperties.getDefault().isSkipEmptyBlock();
        
        blockCounter = new ConcludeStats(GENERATED_BLOCK);
        invalidBlockCounter = new ConcludeStats(GENERATED_INVALID_BLOCK);
        KPICollector.getDefault().addKPI(GENERATED_BLOCK, blockCounter);
        KPICollector.getDefault().addKPI(GENERATED_INVALID_BLOCK, invalidBlockCounter);
    }
	
    protected List<BlockHeader> getUncles(Block pendingBlock) {
        List<BlockHeader> ret = new ArrayList<>();
        long miningNum = pendingBlock.getNumber() + 1;
        Block mineChain = pendingBlock;

        long limitNum = max(0, miningNum - UNCLE_GENERATION_LIMIT);
        Set<ByteArrayWrapper> ancestors = BlockchainImpl.getAncestors(blockStore, pendingBlock, UNCLE_GENERATION_LIMIT + 1, true);
        Set<ByteArrayWrapper> knownUncles = ((BlockchainImpl)blockchain).getUsedUncles(blockStore, pendingBlock, true);
        knownUncles.addAll(ancestors);
        knownUncles.add(new ByteArrayWrapper(pendingBlock.getHash()));

        if (blockStore instanceof IndexedBlockStore) {
            outer:
            while (mineChain != null && mineChain.getNumber() > limitNum) {
                List<Block> genBlocks = ((IndexedBlockStore) blockStore).getBlocksByNumber(mineChain.getNumber());
                if (genBlocks.size() > 1) {
                    for (Block uncleCandidate : genBlocks) {
                        if (!knownUncles.contains(new ByteArrayWrapper(uncleCandidate.getHash())) &&
                                ancestors.contains(new ByteArrayWrapper(blockStore.getBlockByHash(uncleCandidate.getParentHash()).getHash()))) {

                            ret.add(uncleCandidate.getHeader());
                            if (ret.size() >= UNCLE_LIST_LIMIT) {
                                break outer;
                            }
                        }
                    }
                }
                mineChain = blockStore.getBlockByHash(mineChain.getParentHash());
            }
        } else {
            logger.warn("BlockStore is not instance of IndexedBlockStore: generator can't include uncles");
        }
        return ret;
    }
    
	public synchronized PackageResponse generate(PackageRequest request) {
		if (isSkipEmptyBlock && pendingState.hasPendingTransactions()) {
			//skip empty block if enabled.
			return new PackageResponse(null, request.getRound(), 
					request.getPresidentId(), request.getElectionId(), 
					DPOSMessage.ResponseCode.PACKAGE_EMPTY);
		}
		
		AtomicLong taskStartNonce = new AtomicLong(startNonce >= 0 ? startNonce : new Random().nextLong());
		Block bestBlock = blockchain.getBestBlock();
		Block bestPendingBlock = ((PendingStateImpl) pendingState).getBestBlock();
		long time = System.currentTimeMillis() / 1000;
        // adjust time to parent block this may happen due to system clocks difference
        if (bestBlock.getTimestamp() >= time) time = bestBlock.getTimestamp() + 1;
        
        //logger.warn("pre-mine transactions: "+ pendingState.getPendingTransactions().size());
		Block newBlock = blockchain.createNewBlock(bestBlock, pendingState.popupABlockofPendingTxs(), 
				getUncles(bestPendingBlock), time, request.getRound(), request.getPresidentId());
		newBlock.setNonce(ByteUtil.longToBytes(taskStartNonce.longValue()));
		
        logger.info("New block generated !!! {}", newBlock.toSimpleString());
        // broadcast the block
        if (logger.isDebugEnabled()) {
        		logger.debug("Importing newly generated block {} {} ...", newBlock.getShortHash(), newBlock.getNumber());
        }
        ImportResult importResult = ((EthereumImpl) ethereum).addNewMinedBlock(newBlock);
    		if (importResult.isSuccessful()) {
    			if (logger.isDebugEnabled()) {
    				logger.debug("Generated block import result is " + importResult.name());
    			}
    			blockCounter.addNew(1);
    			return new PackageResponse(newBlock.getHeader(), request.getRound(), 
						request.getPresidentId(), request.getElectionId(), 
						DPOSMessage.ResponseCode.SUCCESS);
    		} else {
    			logger.warn("Generated block import result is " + importResult.name());
    			if (importResult == ImportResult.INVALID_BLOCK) {
    				//do something?
    			}
    			invalidBlockCounter.addNew(1);
    			return new PackageResponse(null, request.getRound(), 
						request.getPresidentId(), request.getElectionId(), 
						DPOSMessage.ResponseCode.PACKAGE_FAILURE);
    		}
        
    }
	
}
